﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace Apriori
{
    /// <summary>
    /// Main class for whole project. Holds and calculate all required data.
    /// Its job is to create assotiation rules based on generated T-Tree.
    /// </summary>
    class TTree
    {
        private Market transactions;
        private Itemset root;
        private List<Itemset> FrequentItemsets;
        private double[][] Ddiagram;
        private int[][] Cdiagram;

        public TTree(Market m, Itemset r)
        {
            this.transactions = m;
            this.root = r;
            this.FrequentItemsets = new List<Itemset>();
            this.Ddiagram = new double[r.itemslist.Count][];
            for (int i = 0; i < Ddiagram.Length; i++)
            {
                Ddiagram[i] = new double[r.itemslist.Count];
            }

            this.Cdiagram = new int[r.itemslist.Count][];
            for (int i = 0; i < Cdiagram.Length; i++)
            {
                Cdiagram[i] = new int[r.itemslist.Count];
            }

        }

        public Itemset Root
        {
            set { root = value; }
            get { return this.root; }
        }

        /// <summary>
        /// Building T-tree. Always called with root itemset as parameter.
        /// </summary>
        /// <param name="itset">Root of t-tree.</param>
        public void Build(Itemset itset)
        {
            //Generating all 1-1 Prevelances for further computing.
            CalculeteDdiagram();
            //Generating all 1-1 Cardinalities for further computing.
            CalculeteCdiagram();
            //Recursivly generating t-tree.
            PreBuild(itset);
        }

        private void PreBuild(Itemset itset)
        {
            for (int pid = 0; pid < itset.itemslist.Count; pid++ )
            {
                if (itset.itemslist[pid].Prevelance == 0) continue;

                Itemset candidate = new Itemset();
                int itpoint = pid;
                if (itpoint + 1 > itset.itemslist.Count) return;

                candidate.itemslist = new List<Product>();
                foreach (Product pk in itset.itemslist)
                {
                    candidate.itemslist.Add(new Product(pk.Next, pk.Previous, pk.Name, pk.Index, pk.Prevelance, pk.Count, pk.Cardinality));
                }

                candidate.itemslist.RemoveRange(0, pid + 1);
                itset.itemslist[pid].Next = candidate;

                foreach (Product p in candidate.itemslist)
                {
                    p.Previous = itset.itemslist[pid];
                }

                for (int pp = 0; pp < candidate.itemslist.Count; pp++)
                {
                    double pnmin = 1.0;
                    int cardmin = Int16.MaxValue;
                    if (pid < itset.itemslist.Count - 1)
                    {
                        pnmin = Math.Min(itset.itemslist[pid].Prevelance, itset.itemslist[pid + 1 + pp].Prevelance);
                        cardmin = Math.Min(itset.itemslist[pid].Cardinality, itset.itemslist[pid + 1 + pp].Cardinality);
                    }
                    candidate.itemslist[pp].Prevelance = Math.Min(pnmin, TakeMinPrevelence(itset.itemslist[pid].Index, candidate.itemslist[pp].Index));
                    candidate.itemslist[pp].Cardinality = Math.Min(cardmin, TakeMinCardinality(itset.itemslist[pid].Index, candidate.itemslist[pp].Index));
                }

                PreBuild(candidate);
            }
            return;
        }

        private double CalculatePrevelance(Product parent, Product candidate)
        {
            int counter = 0;

            foreach (Transaction t in this.transactions.basket)
            {
                 if (t.ToString().Contains(parent.ToString()) && t.ToString().Contains(candidate.ToString()))
                 {
                     counter++;
                 }
            }

                double pr1, pr2;

                if (counter > 0)
                {
                    pr1 = (double)counter / (double)parent.GetCount();
                    pr2 = (double)counter / (double)(candidate.GetCount());



                    //MOC ZBIORU X->Y ! (liczba takich transakcji w ktorych kupiono zarowno X jak Y) <- podstawa obliczenia Confidence
                    candidate.Cardinality = counter;

                }
                else
                {
                    pr1 = 0.0;
                    pr2 = 0.0;
                }

                return Math.Min(parent.Prevelance, Math.Min(pr1, pr2));
        }

        private int CalculateCardinality(Product parent, Product candidate)
        {
            int counter = 0;

            foreach (Transaction t in this.transactions.basket)
            {
                if (t.PrintGoods().Contains(parent.ToString()) && t.PrintGoods().Contains(candidate.ToString()))
                {
                    counter++;
                }
            }

            return counter;
        }


        private void CalculeteDdiagram()
        {
            foreach(double[] ar in this.Ddiagram)
            {
                Array.Clear(ar, 0, ar.Length);
            }

            for (int i = 0; i < Ddiagram.Length; i++)
            {
                for (int j = i; j < Ddiagram.Length; j++)
                {
                    Ddiagram[i][j] = CalculatePrevelance(this.root.itemslist[i], this.root.itemslist[j]);
                    Ddiagram[j][i] = Ddiagram[i][j];
                }
            }

            return;
        }

        private void CalculeteCdiagram()
        {
            foreach (int[] ar in this.Cdiagram)
            {
                Array.Clear(ar, 0, ar.Length);
            }

            for (int i = 0; i < Cdiagram.Length; i++)
            {
                for (int j = i; j < Cdiagram.Length; j++)
                {
                    Cdiagram[i][j] = CalculateCardinality(this.root.itemslist[i], this.root.itemslist[j]);
                    Cdiagram[j][i] = Cdiagram[i][j];
                }
            }

            return;
        }

        private double TakeMinPrevelence(int i, int j)
        {
            return Ddiagram[i][j];
        }

        public void printddiagram()
        {
            foreach (double[] i in Ddiagram)
            {
                Console.WriteLine();
                foreach (double j in i)
                {
                    Console.Write(j + " ");
                }
            }
        }

        private int TakeMinCardinality(int i, int j)
        {
            return Cdiagram[i][j];
        }

        public void Print(Itemset iset)
        {

            foreach (Product p in iset.itemslist)
            {
                Console.Write("\n");
                Console.Write(p.ToString());
                if (p.Next != null)
                {
                    Console.Write(", ");
                    Print(p.Next);
                }
                else
                {
                    Console.Write(" = " + p.Prevelance + "\n");
                }
            }
            return;   
        }

        /// <summary>
        /// Runs through the T-tree in search for all Frequent Itemsets based on given minimal support.
        /// Itemset is Frequent if its Lenght is more than 1 (it's not in a root) and all its elements Prevelances are above given minsupport.
        /// </summary>
        /// <param name="r">Called with root.</param>
        /// <param name="minsupp">Minimal support above which we generate Frequent Itemsets.</param>
        public void GenFreqIsets(Itemset r, double minsupp)
        {
            foreach (Product p in r.itemslist)
            {
                if (p.Prevelance >= minsupp)
                {
                    //dodaje do frequent itemsetu jesli dlugosc tego co zwroci printup > 1
                    Itemset freq = new Itemset(); 
                    freq = PrintIsetUp(p);
                    if (freq.itemslist.Count > 1)
                    {
                        this.FrequentItemsets.Add(freq);
                    }
                    GenFreqIsets(p.Next, minsupp);
                }
            }
        }

        private void PrintUp(Product p, double pre)
        {
            Console.Write(p.Name + ", ");
            while (p.Previous != null)
            {
                p = p.Previous;
                Console.Write(p.Name + ", ");
            }
            Console.Write(" = " + pre + "\n");
            return;
        }

        /// <summary>
        /// From current node generate Itemset consist of all Parents of that node.
        /// Used for generating Frequent Itemsets
        /// </summary>
        /// <param name="p">First Product in generated Itemset.</param>
        /// <returns>Itemset that is essencialy new Frequent Itemset.</returns>
        private Itemset PrintIsetUp(Product p)
        {
            Itemset output = new Itemset();
            output.itemslist.Add(p);
            while (p.Previous != null)
            {
                p = p.Previous;
                output.itemslist.Add(p);
            }
            return output;
        }

        public void PrintFreqents()
        {
            StreamWriter output = new StreamWriter("FrequentItemsets.txt");
            output.WriteLine("Liczba utworzonych zbiorów częstych = " + this.FrequentItemsets.Count);
            output.WriteLine();
            foreach (Itemset i in this.FrequentItemsets)
            {
                output.WriteLine("{ " + i.Print() + " } = " + Math.Round(i.itemslist[0].Prevelance, 3));
            }
            output.Close();
        }


        /// <summary>
        /// For n-element Itemset generates all its permutations. These are added if its count is non-zero nor n-element.
        /// </summary>
        /// <param name="curr">Itemset from which we want to generate permutations.</param>
        /// <returns>List of Itemsets that keeps all non-zero non-n-element permutations.</returns>
        private List<Itemset> GeneratePermutations(Itemset curr)
        {
            List<Itemset> permutations = new List<Itemset>();
            for (int i = 0; i < Math.Pow(2, curr.itemslist.Count); i++)
            {
                Itemset combination = new Itemset();
                for (int j = 0; j < curr.itemslist.Count; j++)
                {
                    if ((i & (1 << (curr.itemslist.Count - j - 1))) != 0)
                    {
                        combination.itemslist.Add(curr.itemslist[j]);
                    }
                }

                if(combination.itemslist.Count != 0 && combination.itemslist.Count != curr.itemslist.Count)
                {
                    permutations.Add(combination);
                }
            }
            return permutations;
        }

        /// <summary>
        /// Its job is to generate all permutations based on Frequent Itemsets stored in this.FrequentItemsets.
        /// </summary>
        public void PrintPermutations()
        {
            foreach (Itemset i in this.FrequentItemsets)
            {
                GeneratePermutations(i);
            }      
        }

        /// <summary>
        /// Generate assoctiation rules above given min confidence.
        /// Also prints generated assoctition rules to .txt file.
        /// </summary>
        /// <param name="minconf">Given minimal confidence.</param>
        public void GenerateRules(double minconf)
        {
            StreamWriter output = new StreamWriter("AssociationRules.txt");
            int rulescounter = 0;
            //dla kazdego zbioru czestego
            foreach (Itemset freq in this.FrequentItemsets)
            {
                //wygeneruj wszystkie permutacje - podzbiory o dlugosci wiekszej od 0 i mniejszej od dlugosci pelnego zbioru
                List<Itemset> pers = new List<Itemset>(GeneratePermutations(freq));

                //dla kazdej permutacji
                foreach (Itemset curper in pers)
                {
                    //wytnij ze zbioru czestego z ktorego pochodzi ta permutacje
                    //tworzac kandydata na regułe asocjacyjna
                    //tj. dla zbioru czestego ABCD i jego permutacji DC, tworzona regula jest postaci
                    //CD ==> AB (gdzie AB powstalo po wycieciu CD z ABCD)

                    //sortujemy permutacje aby latwo dostac sie do jej mocy 
                    //poprzez siegniecie do T-drzewa
                    curper.itemslist = curper.itemslist.OrderBy(x => x.Index).ToList<Product>();
                    //siegnij do drzewa po moc danej permutacji;
                    int permcard = GetCardinality(curper);

                    //wycinamy z aktualnego zbioru czestego aktualna permutacje
                    //w celu utworzenia reguly asocjacyjnej
                    Itemset rule = new Itemset();
                    rule.itemslist = CutOff(freq.itemslist, curper.itemslist);

                    //korzystamy z tego ze pole Prevelance w pierwszym elemencie (tj. elemencie pod indeksem 0) kazdego zbioru czestego zawiera
                    //support dla calego zbioru
                    double suprule = freq.itemslist[0].Prevelance;
                    Itemset tempcard = new Itemset();
                    tempcard.itemslist = freq.itemslist.OrderBy(x => x.Index).ToList<Product>();
                    int cardrule = GetCardinality(tempcard);

                    //obliczamy Confidence dla danej reguly zgodnie ze wzorem
                    //conf = card(X->Y) / card(X)
                    double ruleconfidence = (double) cardrule / (double) permcard;

                    if (ruleconfidence >= minconf)
                    {
                        output.WriteLine(curper.ToString() + " ==> " + rule.ToString() + " | Sup = " + Math.Round(freq.itemslist[0].Prevelance, 3) + " | Conf = " + Math.Round(ruleconfidence, 3));
                        rulescounter++;
                    }
                }
            }
            output.Close();
            WriteToStartOfFile("AssociationRules.txt", "Liczba utworzonych regul asocjacyjnych = " + rulescounter.ToString());
        }

        /// <summary>
        /// method for adding new data to the beginning of a file.
        /// ==> taken from the Internet for quick solution.
        /// </summary>
        /// <param name="file">the file we're adding to</param>
        /// <param name="newValue">the value we want to write</param>
        private void WriteToStartOfFile(string file, string newValue)
        {

            char[] buffer = new char[2048];



            string tempFile = file + ".tmp";

            File.Move(file, tempFile);



            using (StreamReader reader = new StreamReader(tempFile))
            {

                using (StreamWriter writer = new StreamWriter(file, false))
                {

                    writer.WriteLine(newValue);



                    int totalRead;

                    while ((totalRead = reader.Read(buffer, 0, buffer.Length)) > 0)

                        writer.Write(buffer, 0, totalRead);

                }


                reader.Close();
                File.Delete(tempFile);

            }

        }

        private double GetSupport(Itemset c)
        {
            Product current = new Product();
            //current na poczatku ustawiamy na element w root'cie odpowiadajacy pierwszemu elementowi z permutacji
            current = this.root.itemslist[c.itemslist[0].Index];

            for (int i = 1; i < c.itemslist.Count; i++)
            {
                //w kazdym kolejnym kroku ustawiamy 'current' na produkt o tym samym indeksie (tj. na taki sam produkt) w danym itemsecie
                int j = 0;
                while (current.Next.itemslist[j].Index != c.itemslist[i].Index) { j++; }
                current = current.Next.itemslist[j];
            }

            return current.Prevelance;
        }

        private int GetCardinality(Itemset c)
        {
            Product current = new Product();
            //current na poczatku ustawiamy na element w root'cie odpowiadajacy pierwszemu elementowi z permutacji
            current = this.root.itemslist[c.itemslist[0].Index];

            for (int i = 1; i < c.itemslist.Count; i++)
            {
                //w kazdym kolejnym dla danego itemsetu szukamy produktu w tym itemsecie o takim samy product.index jak nasz current.index
                int j = 0;
                while (current.Next.itemslist[j].Index != c.itemslist[i].Index) { j++; }
                current = current.Next.itemslist[j];
            }

            return current.Cardinality;
        }

        private List<Product> CutOff(List<Product> cutfrom, List<Product> cutme)
        {
            List<Product> output = new List<Product>();

            foreach (Product p in cutfrom)
            {
                if (!cutme.Contains(p))
                {
                    output.Add(p);
                }
            }

            return output;
        }
    }
}
